package openblocks.common;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.SoundCategory;
import openmods.Log;
public class BeepGenerator {
private static final int SAMPLE_RATE = 44100;
private static final int SAMPLES_PER_BUFFER = SAMPLE_RATE / 8;
private static final int BYTES_PER_SAMPLE = 2;
private static final int BYTES_PER_BUFFER = BYTES_PER_SAMPLE * SAMPLES_PER_BUFFER;
private static final double BUFFER_DURATION = (double)SAMPLES_PER_BUFFER / SAMPLE_RATE;
private static final double FREQUENCY_MAX_CHANGE_PER_BUFFER_DURATION = 50.0;
private final byte[] scratchBuffer = new byte[BYTES_PER_BUFFER];
private static final byte[] ZERO_BUFFER = new byte[BYTES_PER_BUFFER];
private short volume = 2560;
private double wavePhase;
private int beepPhase;
private double toneFrequency;
private double targetToneFrequency;
private double beepFrequency;
private int samplesPerBeep;
private class WriterThread extends Thread {
private final SourceDataLine line;
private boolean running = true;
public WriterThread(SourceDataLine line) {
this.line = line;
setDaemon(true);
setName("Beeper thread");
}
@Override
public void run() {
line.start();
try {
while (running) {
final int available = line.available();
if (available >= SAMPLES_PER_BUFFER)
writeSample(line);
try {
Thread.sleep(100); // has to be lower than SAMPLES_PER_BUFFER / SAMPLE_RATE
} catch (InterruptedException e) {
running = false;
}
}
} finally {
running = false;
line.close();
}
}
public boolean isShuttingDown() {
return !running;
}
public void shutdown() {
running = false;
}
}
private WriterThread writerThread;
public synchronized void start() {
wavePhase = 0;
beepPhase = 0;
if (!isRunning()) {
final AudioFormat af = new AudioFormat(SAMPLE_RATE, 8 * BYTES_PER_SAMPLE, 1, true, true);
try {
SourceDataLine line = AudioSystem.getSourceDataLine(af);
line.open(af, SAMPLE_RATE);
writerThread = new WriterThread(line);
writerThread.start();
} catch (LineUnavailableException e) {
Log.warn(e, "Failed to initialize beeper");
if (writerThread != null) writerThread.shutdown();
}
}
}
public synchronized void stop() {
if (writerThread != null) writerThread.shutdown();
setTargetToneFrequency(0d);
setBeepFrequency(0d);
}
public synchronized boolean isRunning() {
return writerThread != null && writerThread.isAlive() && !writerThread.isShuttingDown();
}
private void writeSample(SourceDataLine line) {
final double lastToneFrequency;
if (this.toneFrequency == 0d || this.targetToneFrequency == 0d) {
lastToneFrequency = this.targetToneFrequency;
this.toneFrequency = this.targetToneFrequency;
} else {
lastToneFrequency = this.toneFrequency;
final double delta = this.targetToneFrequency - this.toneFrequency;
this.toneFrequency += limit(delta, FREQUENCY_MAX_CHANGE_PER_BUFFER_DURATION);
}
byte[] buffer = generateSamplesWithSweep(lastToneFrequency, this.toneFrequency);
line.write(buffer, 0, buffer.length);
}
private static double limit(double value, double limit) {
if (value < 0)
return Math.max(value, -limit);
else
return Math.min(value, limit);
}
private byte[] generateSamplesWithSweep(double f0, double f1) {
if (f0 == 0.0 && f1 == 0.0)
return ZERO_BUFFER;
final float masterSoundLevel = Minecraft.getMinecraft().gameSettings.getSoundLevel(SoundCategory.MASTER);
if (masterSoundLevel == 0)
return ZERO_BUFFER;
final float amplitude = Math.max(volume * masterSoundLevel, 2);
final double sweepDuration = BUFFER_DURATION;
// see 'chirp' on wiki for explanation of constants
final double k = (f1 - f0) / sweepDuration;
int sampleCount = 0;
if (samplesPerBeep == 0) {
for (int i = 0; i < BYTES_PER_BUFFER; i += BYTES_PER_SAMPLE, sampleCount++) {
final short v = (short)calculateSample(amplitude, wavePhase, f0, k, sampleToRealTime(sampleCount));
writeShortSample(scratchBuffer, i, v);
}
} else {
int beepSample = beepPhase;
for (int i = 0; i < BYTES_PER_BUFFER; i += BYTES_PER_SAMPLE, sampleCount++) {
if (beepSample < samplesPerBeep) {
final short v = (short)calculateSample(amplitude, wavePhase, f0, k, sampleToRealTime(sampleCount));
writeShortSample(scratchBuffer, i, v);
} else {
scratchBuffer[i] = 0;
scratchBuffer[i + 1] = 0;
}
if (beepSample++ >= 2 * samplesPerBeep)
beepSample = 0;
}
beepPhase = beepSample;
}
wavePhase = phase(wavePhase, f0, k, sampleToRealTime(sampleCount)) % (2 * Math.PI);
return scratchBuffer;
}
private static void writeShortSample(byte[] buf, int i, final short v) {
buf[i] = (byte)(v >> 8);
buf[i + 1] = (byte)(v);
}
private static double sampleToRealTime(int sampleCount) {
return (double)sampleCount / SAMPLE_RATE;
}
private static double calculateSample(float amplitude, double phase0, double f0, double k, double t) {
return (short)(amplitude * Math.sin(phase(phase0, f0, k, t)));
}
private static double phase(double phase0, double f0, double k, double t) {
return phase0 + 2.0 * Math.PI * (f0 + k / 2.0 * t) * t;
}
public short getVolume() {
return volume;
}
public void setVolume(short volume) {
this.volume = volume;
}
public double getToneFrequency() {
return toneFrequency;
}
public double getTargetToneFrequency() {
return this.targetToneFrequency;
}
public void setTargetToneFrequency(double frequency) {
this.targetToneFrequency = frequency;
}
public double getBeepFrequency() {
return beepFrequency;
}
public void setBeepFrequency(double beepFrequency) {
this.beepFrequency = beepFrequency;
if (beepFrequency == 0) {
samplesPerBeep = 0;
} else {
samplesPerBeep = (int)(SAMPLE_RATE / beepFrequency);
}
}
}